/*
 **********************************************************************
 * Copyright (c) 2013 Kernull.com
 * 
 * All rights reserved. No warranty, explicit or implicit, provided.
 * 
 * @author 	Christopher Aprea <caprea@kernull.com>
 * @date 	Oct 8, 2013
 * @project javapoker
 * 
 **********************************************************************
 */

package javapoker;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map.Entry;

import javapoker.Card.CRank;
import javapoker.Card.CSuit;

// TODO: Auto-generated Javadoc
/**
 * The Class Hand.
 */
public class Hand implements Comparable<Hand>
{

	//The size of a valid poker hand
	/** The full hand size. */
	public final int FULL_HAND_SIZE = 5;
	
	/** The _hand. */
	ArrayList<Card> _hand = null;
	
	/** The _sorted. */
	private boolean _sorted = false;
	
	/** The _pairs. */
	private HashMap<CRank, Integer> _pairs = null;
	
	
	private Card _seqPowerCard = null; //This is the highest ranking card within the sequence, not the same as the kicker
	private Card _pairPowerCard = null;
	
	/**
	 * Checks if is sorted.
	 *
	 * @return true, if is sorted
	 */
	public boolean isSorted() 
	{
		return _sorted;
	}


	/**
	 * Instantiates a new hand.
	 */
	public Hand() 
	{
		init();
	}
	

	public Hand(ArrayList<Card> cards)
	{
		init(cards);
	}
	
	public Collection<Card> getCards()
	{
		return _hand;
	}
	
	/**
	 * Conveiniance method that wraps Hand() for creating hand son the fly for JUnits
	 * @param input
	 */
	public Hand(String input)
	{
		final String delim = " ";
		//Convert the input into uppercase
		input = input.trim().toUpperCase();
		
		String multi[] = input.split(delim);
		
		init(); //create a new hand
		
		//add cards to the hand
		Card c = null;
		for (String single : multi)
		{
			c = new Card(single);
			addCard(c);
		}
	}
	
	private void init()
	{
		init(new ArrayList<Card>());
	}
	
	private void init(ArrayList<Card> cards)
	{
		//create an empty hand
		_hand = cards;
		_pairs = new HashMap<Card.CRank, Integer>();
		cards.isEmpty(); //check for null
	}

	/**
	 * Clear.
	 */
	public void clear()
	{
		//_extendedHand = null;
		_hand.clear();
		_sorted = false;
		_pairs.clear();
		_seqPowerCard = null;
	}
	
	public Card get(int n)
	{
		return _hand.get(n);
	}
	
	/**
	 * Gets the size of the hand.
	 *
	 * @return The size of the list of cards
	 */
	public int size() 
	{
		return _hand.size();
	}
	
	/**
	 * Adds the cards.
	 *
	 * @param cards the cards
	 */
	public void addCards(Collection<Card> cards)
	{
		if (null == cards)
		{
			throw new IllegalArgumentException("Cards to be added are null");
		}
		else
		{
			_hand.addAll(cards);
		}
		_sorted = false;
	}
	
	/**
	 * Adds a hand to another hand. Useful for Texas holdem
	 * @param h
	 */
	public void addCards(Hand h)
	{
		addCards(h._hand);
	}
	
	/**
	 * Combine hands.
	 *
	 * @param h the h
	 */
	public static Hand combineHands(Hand left, Hand right)
	{
		Hand combined = new Hand();
		
		combined.addCards(left._hand);
		combined.addCards(right._hand);
		
		return combined;
	}
	
	/**
	 * Adds the card.
	 *
	 * @param single the single
	 */
	public void addCard(Card single)
	{
		ArrayList<Card> list = new ArrayList<Card>();
		list.add(single);
		addCards(list);
	}
	
	//TODO: not complimented
	/**
	 * Does nothing!.
	 */
	public void discard()
	{
		_hand.clear();
	}
	
	/* (non-Javadoc)
	 * @see java.lang.Comparable#compareTo(java.lang.Object)
	 */
	@Override
	public int compareTo(Hand o) 
	{
		if ( getQuality() > o.getQuality())
		{
			return 1;
		}
		else if ( getQuality() < o.getQuality())
		{
			return -1;
		}
		else
		{
			return 0;
		}
	}

   /**
    * Gets the quality.
    *
    * @return the quality
    */
   public double getQuality()
   {
	   double tier = 0;
	   
	 
	   findPairs();
	 
	   /*
	    * Powers of 15 for ranking
	    */
	   // 15 ^ X
	   int base = CRank.values().length + 1;
	   double powers[] = new double[base];
	   
	   for ( int p=0; p < base; p++)
	   {
		   powers[p] = Math.pow(base, p+1);
	   }
	   
	   //To distinguish between two hands that are similar
	   //look over the occurances number to find the biggest
	   double pairmult = findPairPower();
	   int seqmult = findSequencePower();
	   int kick = getKicker().asInt();
	   
	   if ( hasStraightFlush() )
		   tier = powers[9];
	   else if ( hasNofaKind(4) )
		   tier = powers[8];
	   else if ( hasFullHouse() )
		   tier = powers[7];
	   else if ( hasFlush() )
		   tier = powers[6];
	   else if ( hasStraight() )
		   tier = powers[5];
	   else if ( hasTriple() )
		   tier = powers[4];
	   else if ( hasTwoPair() )
		   tier = powers[3];
	   else if ( hasPair() )
		   tier = powers[2];
	   else
		   tier = 1;
	   	
	   tier = tier + (base * pairmult) + (base * seqmult) + kick;
	   return tier;
   }
   
   public String toNamedPokerHand()
   {
	   
	   String msg = "";
	   
	   
	   if ( hasStraightFlush() )
		   msg = "Straight Flush";
	   else if ( hasNofaKind(4) )
		   msg = "Four of a Kind";
	   else if ( hasFullHouse() )
		   msg = "Full House";
	   else if ( hasFlush() )
		   msg = "Flush";
	   else if ( hasStraight() )
		   msg = "Straight";
	   else if ( hasTriple() )
		   msg = "Three of a Kind";
	   else if ( hasTwoPair() )
		   msg = "Two Pair";
	   else if ( hasPair() )
		   msg = "Pair";
	   else
		   msg = "High Card";
	   
	   
	   for ( CRank r : _pairs.keySet())
	   {
		   msg += " (" + r.toString() + ")";
	   }
	   
	   msg += " " + getKicker().toString() + " kicker.";
	   
	   
	   
	   
	   return msg;
   }
   
   public CRank getKicker() 
   {
	   //get last card
	   int s = _hand.size();
	   CRank v = CRank.UNKNOWN;
	   if ( s >= 1)
	   {
		   v = _hand.get(s-1).getRank();
	   }
	   
	   return v;
   }


/**
    * Checks for flush.
    *
    * @return true, if successful
    */
   public boolean hasFlush()
   {
	   return hasFlush(FULL_HAND_SIZE);
   }
   
   /**
    * Checks for flush.
    *
    * @param n the n
    * @return true, if successful
    */
   public boolean hasFlush(int n)
   {
	   /*Counts map used to count how many of each suit 
	    * not just counting that all cards are of the same suit
	    */

	   for (CSuit s : CSuit.values())
	   {
		   int count = 0;
		   for (Card c : _hand) 
		   {
			   if ( s.equals(c.getSuit()) )
			   {
				   count++;
				   if ( count >= n)
				   {
					   _seqPowerCard = c;
					   return true;
				   }
			   }
		   }
	   }
	   
	   return false;
   }
   
   /**
    * Wrapper for this.hasStraight(FULL_HAND_SIZE)
    *
    * @return true, if successful
    */
   public boolean hasStraight()
   {
	   return hasStraight(FULL_HAND_SIZE);
   }
   
   /**
    * Anytime the list has changed it should be sorted, but not excessively.
    */
   public void sort()
   {
	   if ( !_sorted )
	   {
		   Collections.sort(_hand);
		   _sorted = true;
		   //System.out.println("Sorted hand " + this.toString());
	   }
	   
	   //System.out.println("Did not sort, because it should already be sorted " + this.toString());
		   
	   
   }
   
   
   /**
    * Checks for straight flush.
    *
    * @return true, if successful
    */
   public boolean hasStraightFlush()
   {
	   return findStraight(FULL_HAND_SIZE, true);
   }
   
   /**
    * Checks for straight.
    *
    * @param num Number of cards required in sequence to be valid
    * @return true, if successful
    */
   public boolean hasStraight(int num)
   {
	   return findStraight(num, false);
   }
   
   
   /**
    * Prerequisite: Hand must be sorted.
    *
    * @param num Number of cards required to be in sequence
    * @param requireSuit If true requires the straight to be in the same suit
    * @return True if there is a sequence of num cards
    */
   private boolean findStraight(int num, boolean requireSuit)
   {
	   
	   //Short circuit to avoid looking for a straight in an empty hand.
	   if ( _hand.size() < 1) return false;
	   
	   sort();
	   int previous= _hand.get(0).getRank().asInt(); //subtract one so the foreach loop is conveiniant
	   int current = previous;						//current cards facevalue to compare with previous card
	   CSuit activeSuit = null;
	   
	   int count = 1;
	   for (int i=1; i < _hand.size(); i++) //start with the second card
	   {
		   current = _hand.get(i).getRank().asInt();
		   
		   if ( previous+1 == current)
		   {
			   //check if forcing suits to be the same
			   if ( requireSuit )
			   {
				   if ( _hand.get(i).getSuit() == activeSuit)
				   {
					   count++;
				   }
			   }
			   else
			   {	//not requiring the suits to be the same
				   count++;
			   }
			   //save the power card
			   _seqPowerCard = _hand.get(i);
			   
		   }
		   else if ( current == previous)
		   {
			   //hand has two cards of the same rank, but different suits
			   //doesn't count towards the straight, but does not reset the counter
		   }
		   else
		   {
			   //Reset the sequence (for large hands, keep looking)
			   _seqPowerCard = null;
			   count = 1;
			   activeSuit = _hand.get(i).getSuit();
		   }
		   previous = current;
	   }
	   
	   
	   
	   
	   if ( count >= num) return true;
	   
	   return false;
   }
   
   /**
    * Contains.
    *
    * @param r the r
    * @param s the s
    * @return true, if successful
    */
   public boolean contains(CRank r, CSuit s)
   {
	   //Cannot use Collections.contains() because that is matching the object refer. explicitly
	   if ( null != s && null != r)
	   {
		   Card dummy = new Card(r, s);
		   Card current;
		   for (int i=0; i < _hand.size(); i++)
		   {
			   current = _hand.get(i);
			   if ( current.compareTo(dummy) == 0)
			   {
				   return true;
			   }
		   }
	   }
	   return false;
   }
   
   /**
    * Returns true if card matching R's rank is found.
    *
    * @param r face value of card to find
    * @return true/false
    */
   public boolean containsRank(CRank r)
   {
	   if ( contains(r, CSuit.HEARTS)) return true;
	   if ( contains(r, CSuit.DIAMONDS)) return true;
	   if ( contains(r, CSuit.CLUBS)) return true;
	   if ( contains(r, CSuit.SPADES)) return true;
	   
	   return false;
   }

   private int findSequencePower()
   {
	   if ( null != _seqPowerCard ) 
		   return _seqPowerCard.getRank().asInt();
		
	   else
		   return 0;
	   
		   
   }
   
	/**
	 * Used to find the 'high' pair of cards in the set of N-of-A-Kind
	 * @return
	 */
   private double findPairPower()
   {

	   //Find the power card
	   Iterator<Entry<CRank, Integer>> i = _pairs.entrySet().iterator();
	   
	   double num = 0;
	   double magic = 0;
	   int base = CRank.values().length;
	   while ( i.hasNext())
	   {
		   //Get each entry in the key/value set
		   Entry<CRank, Integer> occur = i.next();
		   
		   
		   //Create a temporary card of unknown suit because there is duplicates
		   // Face ^ Occurances
		   magic = Math.pow( base, occur.getValue().longValue() ); //number of times
		   num += occur.getKey().asInt() * magic; //raise the power of the suit to the occurances of it

		   //System.out.println(occur.getKey() + "" + occur.getValue() + ": " + magic + " * " + occur.getKey().asInt());
		   

		   
		   //raise the power of the suit to the occurances of it
	   }
	   
	   num = num;
	   
	   return num;
   }
   
   /**
    * Prerequisite: must run findPairs() on sorted hand.
    *
    * @return true, if successful
    */
   public boolean hasFullHouse()
   {
	   boolean three = false;
	   boolean two = false;
	   
	   if ( _pairs.keySet().size() >= 2)
	   {

		   //Look for 3 of a kind (or more) for larger hands
		   for ( int v : _pairs.values())
		   {
			   if ( v >= 3) 
			   {
				   three = true;
			   }
		   }
		   
		   //look for 2 of a kind
		   for ( int v : _pairs.values())
		   {
			   if ( v == 2) two = true;
		   }
	   }
	   
	   
	   
	   //If there is an occurance of both ThreeOfKind and TwoOfKind
	   if ( three && two) return true;
	   
	   return false;
   }
   
   /**
    * Checks for triple.
    *
    * @return true, if successful
    */
   public boolean hasTriple()
   {
	   return hasNofaKind(3);
   }

   /**
    * Checks for nofa kind.
    *
    * @param n the n
    * @return true, if successful
    */
   public boolean hasNofaKind(int n)
   {
	   if ( _pairs.keySet().size() >= 0)
	   {
		   for ( int v : _pairs.values())
		   {
			   if ( v >= n)   return true;
		   }
	   }
	   return false;
   }
   
   /**
    * Checks for two pair.
    *
    * @return true, if successful
    */
   public boolean hasTwoPair()
   {
	   return hasNPairs(2);
   }
   
   /**
    * Checks for n pairs.
    *
    * @param n the n
    * @return true, if successful
    */
   public boolean hasNPairs(int n)
   {
	   if ( _pairs.keySet().size() >= n)
	   {
		   return true;
	   }
	   return false;
   }
   
   /**
    * Checks for pair.
    *
    * @return true, if successful
    */
   public boolean hasPair()
   {
	   return hasNofaKind(2);
   }
   
   
   /**
    * Abbreviated string.
    *
    * @return the string
    */
   public String abbreviatedString()
   {
	   String msg = "[";
	   for(Card c: _hand)
	   {
		   msg = msg + c.toShortString() + " ";
	   }
	   
	   msg = msg + "]";
	   return msg;
   }
   
   /**
    * Count the occurances of similarly valued cards that appear at least twice
    * Looks through hand to find if there are multiple occurances of the
    * same face value. Stores the results in _pairs for later retrieval
    *
    * @return the hash map
    */
   public HashMap<CRank, Integer> findPairs() 
   {
	   
	   sort();
	   _pairs.clear();
	   
	   //Stores all the occurances of found pairs
	   HashMap<CRank, Integer> found = new HashMap<CRank, Integer>();
	   
	   
	   if ( _hand.size() < 1) 
	   {
		   return found;
	   }
	   
	   CRank previous = _hand.get(0).getRank();
	   CRank current = _hand.get(0).getRank();
	   
	   int value = 0;
	   
	   //Skip the first card
	   for (int i=1; i < _hand.size(); i++)
	   {
		   current = _hand.get(i).getRank();
		   
		   //This card is like a previous card, incriment the counts
		   if ( current == previous )
		   {

			   //incriment the previously existing count
			   if ( found.containsKey(current))
			   {
				   value = found.get(current); //get previous value
				   value++;					//incriment value
				   found.put(current, value); //replace existing value
			   }
			   else
			   {
				 
				 /*
				  * because this is the first entry of the PAIR, 
				  * the record value is the number of similar cards seen
				  */
				 found.put(current, 2); //there is no previous record of the pair, make one  
			   }
		   }
		   
		   previous = current;
	   }
	   
	   _pairs = found;
	   return found;
	   
   }
   
}
